package org.xx.sipahi.controllers;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import org.xx.armory.commons.ForLogging;
import org.xx.armory.spring5.data.PageLinkCreator;
import org.xx.armory.spring5.jpa.PredicateBuilder;
import org.xx.armory.spring5.mvc.OutputTaskLog;
import org.xx.armory.spring5.mvc.ResourceNotFoundException;
import org.xx.sipahi.Category;
import org.xx.sipahi.Notice;
import org.xx.sipahi.jpa.CategoryPO_;
import org.xx.sipahi.jpa.CategoryRepository;
import org.xx.sipahi.jpa.NoticeContentPO;
import org.xx.sipahi.jpa.NoticeContentRepository;
import org.xx.sipahi.jpa.NoticePO;
import org.xx.sipahi.jpa.NoticePO_;
import org.xx.sipahi.jpa.NoticeRepository;

import javax.persistence.criteria.JoinType;
import java.security.Principal;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.LongStream;

import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.xx.armory.spring5.mvc.ModelUtils.JSON_VIEW;
import static org.xx.armory.spring5.mvc.ModelUtils.paramNonNegative;
import static org.xx.armory.spring5.mvc.ModelUtils.paramNotBlank;
import static org.xx.armory.spring5.mvc.ModelUtils.parsePageArgs;
import static org.xx.sipahi.AbstractPage.DRAFT;
import static org.xx.sipahi.AbstractPage.PUBLISHED;

@Controller
@RequestMapping("/notice")
public class NoticeController {
    @ForLogging
    private final Logger logger = LoggerFactory.getLogger(NoticeController.class);

    @Autowired
    private CategoryRepository categoryRepository;

    @Autowired
    private NoticeRepository noticeRepository;

    @Autowired
    private NoticeContentRepository noticeContentRepository;

    private Map<String, String> findCategories() {
        final var result = new LinkedHashMap<String, String>();
        this.categoryRepository.findAllKeyValue(CategoryRepository.NOTICE)
                               .forEach(row -> {
                                   if (row[0] != null) {
                                       result.put(row[0].toString(), row[1].toString());
                                   }
                               });
        return result;
    }

    @GetMapping(value = {"/index", "/index({args})"})
    @Transactional(readOnly = true)
    public ModelAndView index(
            @PathVariable(required = false) String args,
            @RequestParam(defaultValue = "") String page
    ) {
        final var form = new NoticeSearchForm(args);
        final var pageArgs = parsePageArgs(page);

        final var result = new ModelAndView();

        result.addObject("allCategories", findCategories());
        result.addObject("form", form);
        final var noticePage = this.noticeRepository.findAll(
                (root, cq, cb) -> {
                    if (NoticePO.class.isAssignableFrom(cq.getResultType())) {
                        root.fetch(NoticePO_.category, JoinType.LEFT);
                    }
                    return new PredicateBuilder()
                            .add(() -> form.getCategoryId() != null, cb.equal(root.get(NoticePO_.category).get(CategoryPO_.id), form.getCategoryId()))
                            .add(() -> !form.getTitle().isBlank(), cb.like(root.get(NoticePO_.title), "%" + form.getTitle() + "%"))
                            .add(() -> !form.getKeyword().isBlank(), cb.like(root.get(NoticePO_.keywords), "%" + form.getKeyword() + "%"))
                            .add(() -> !form.getUpdater().isBlank(), cb.equal(root.get(NoticePO_.updater), form.getUpdater()))
                            .add(() -> form.getStartDate() != null, cb.greaterThanOrEqualTo(root.get(NoticePO_.lastModifiedTime), form.getStartDate()))
                            .add(() -> form.getEndDate() != null, cb.lessThanOrEqualTo(root.get(NoticePO_.lastModifiedTime), form.getEndDate()))
                            .build(cb);
                }, pageArgs.desc(NoticePO_.lastModifiedTime.getName()));
        result.addObject("notices", noticePage.map(NoticeRepository::toNotice).toList());
        result.addObject("noticeCount", noticePage.getTotalElements());
        result.addObject("pageLinkCreator", new PageLinkCreator());
        result.addObject("page", pageArgs);
        result.setViewName("/notice/index");

        return result;
    }

    @GetMapping("/create")
    @Transactional(readOnly = true)
    public ModelAndView setUpCreate() {
        final var result = new ModelAndView();

        final var now = new Date();

        result.addObject("allCategories", findCategories());
        result.addObject("notice", new Notice(
                null, // id
                "", // title
                Category.EMPTY, // categoryId
                "", // keywords
                "", // brief
                "", // creator
                now, // createTime,
                "", // updater
                now, // lastModifiedTime
                Notice.DRAFT, // status
                false, // star
                null, // stickTime,
                ""  // content
        ));

        return result;
    }

    @PostMapping("/create")
    @OutputTaskLog(category = "NOTICE",
                   action = "CREATE_NOTICE")
    @Transactional
    public ModelAndView create(
            @RequestParam(name = "category.id") Integer categoryId,
            @RequestParam String title,
            @RequestParam String keywords,
            @RequestParam String brief,
            @RequestParam String content,
            Principal principal
    ) {
        paramNonNegative(categoryId, "category");
        paramNotBlank(title, "title");

        final var now = new Date();

        final var noticePO = new NoticePO();
        noticePO.setTitle(title);
        noticePO.setCategory(this.categoryRepository.getOneOrNull(categoryId));
        noticePO.setKeywords(keywords);
        noticePO.setBrief(brief);
        noticePO.setCreator(principal.getName());
        noticePO.setCreateTime(now);
        noticePO.setStatus(Notice.COMMITTED);
        noticePO.setStar(false); // 不设置星标。
        noticePO.setUpdater(principal.getName());
        noticePO.setLastModifiedTime(now);
        this.noticeRepository.saveAndFlush(noticePO);

        final var noticeContentPO = new NoticeContentPO();
        noticeContentPO.setId(noticePO.getId());
        noticeContentPO.setContent(content);
        this.noticeContentRepository.saveAndFlush(noticeContentPO);

        return new ModelAndView("redirect:/notice/index");
    }

    @GetMapping("/{id:^\\d+$}")
    @Transactional(readOnly = true)
    public ModelAndView setUpEdit(
            @PathVariable long id
    ) {
        paramNonNegative(id, "id");

        final var result = new ModelAndView("/notice/edit");

        result.addObject("allCategories", findCategories());

        final var noticePO = this.noticeRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(id));
//        if (noticePO.getStatus() != DRAFT && noticePO.getStatus() != COMMITTED) {
//            // 只有草稿状态的页面才可以编辑。
//            throw new BusinessLogicException(2);
//        }
        final var noticeContentPO = this.noticeContentRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(id));

        result.addObject("notice", NoticeRepository.toNotice(noticePO, noticeContentPO));

        return result;
    }

    @PostMapping("/{id:^\\d+$}")
    @OutputTaskLog(category = "NOTICE",
                   action = "UPDATE_NOTICE")
    @Transactional
    public ModelAndView edit(
            @PathVariable long id,
            @RequestParam(name = "category.id") int categoryId,
            @RequestParam String title,
            @RequestParam String keywords,
            @RequestParam String brief,
            @RequestParam String content,
            Principal principal
    ) {
        paramNonNegative(categoryId, "categoryId");
        paramNotBlank(title, "title");

        final var now = new Date();

        final var noticePO = this.noticeRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(id));
//        if (noticePO.getStatus() != DRAFT && noticePO.getStatus() != COMMITTED) {
//            // 只有草稿状态的页面才可以编辑。
//            throw new BusinessLogicException(2);
//        }
        noticePO.setTitle(title);
        noticePO.setCategory(this.categoryRepository.getOneOrNull(categoryId));
        noticePO.setKeywords(keywords);
        noticePO.setBrief(brief);
        noticePO.setUpdater(principal.getName());
        noticePO.setLastModifiedTime(now);

        final var noticeContentPO = this.noticeContentRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(id));
        noticeContentPO.setContent(content);

        this.noticeRepository.flush();
        this.noticeContentRepository.flush();

        return new ModelAndView("redirect:/notice/index");
    }

    private LongStream idStream(
            String idArray
    ) {
        if (idArray == null || idArray.isEmpty()) {
            return LongStream.empty();
        } else {
            final var idArray_ = asList(idArray.split(","));
            Collections.reverse(idArray_);
            return idArray_.stream()
                           .filter(StringUtils::isNotBlank)
                           .mapToLong(NumberUtils::toLong)
                           .filter(id -> id > 0L);
        }
    }

    @DeleteMapping("/{idArray:^[0-9,]+$}")
    @Transactional
    public ModelAndView delete(
            @PathVariable String idArray
    ) {
        paramNotBlank(idArray, "idArray");

        final var result = new ModelAndView(JSON_VIEW);

        result.addObject("result", idStream(idArray).mapToObj(id -> {
            final var po = this.noticeRepository.getOne(id);
            this.noticeRepository.delete(po);
            logger.trace("remove {}", po.getId());
            return new Notice(id);
        }).collect(toList()));

        return result;
    }

    @PutMapping("/sticky/{idArray:^[0-9,]+$}")
    @Transactional
    public ModelAndView setSticky(
            @PathVariable String idArray
    ) {
        paramNotBlank(idArray, "idArray");

        final var result = new ModelAndView(JSON_VIEW);

        final var now = new Date();

        result.addObject("result", idStream(idArray)
                .mapToObj(id -> {
                              final var po = this.noticeRepository.getOne(id);
                              po.setStickTime(now);
                              this.noticeRepository.flush();
                              return NoticeRepository.toNotice(po);
                          }
                ).collect(toList()));

        return result;
    }

    @DeleteMapping("/sticky/{idArray:^[0-9,]+$}")
    @Transactional
    public ModelAndView unsetSticky(
            @PathVariable String idArray
    ) {
        paramNotBlank(idArray, "idArray");

        final var result = new ModelAndView(JSON_VIEW);

        result.addObject("result", idStream(idArray)
                .mapToObj(id -> {
                              final var po = this.noticeRepository.getOne(id);
                              po.setStickTime(null);
                              this.noticeRepository.flush();
                              return NoticeRepository.toNotice(po);
                          }
                ).collect(toList()));

        return result;
    }

    @PutMapping("/publication/{idArray:^[0-9,]+$}")
    @Transactional
    public ModelAndView publish(
            @PathVariable String idArray
    ) {
        paramNotBlank(idArray, "idArray");

        final var result = new ModelAndView(JSON_VIEW);

        result.addObject("result", idStream(idArray)
                .mapToObj(id -> {
                              final var po = this.noticeRepository.getOne(id);
                              po.setStatus(PUBLISHED);
                              this.noticeRepository.flush();
                              return NoticeRepository.toNotice(po);
                          }
                ).collect(toList()));

        return result;
    }

    @DeleteMapping("/revoke/{idArray:^[0-9,]+$}")
    @Transactional
    public ModelAndView revoke(
            @PathVariable String idArray
    ) {
        paramNotBlank(idArray, "idArray");

        final var result = new ModelAndView(JSON_VIEW);

        result.addObject("result", idStream(idArray)
                .mapToObj(id -> {
                              final var po = this.noticeRepository.getOne(id);
                              po.setStatus(DRAFT);
                              this.noticeRepository.flush();
                              return NoticeRepository.toNotice(po);
                          }
                ).collect(toList()));

        return result;
    }
}
